package com.googlecode.vmock;

import com.googlecode.vmock.exceptions.MockVerifyException;
import com.googlecode.vmock.utils.Chain;

/**
 * Class that remembers all expected call results that were registered for a certain call intercepted in {@link MockStage#RECORDING} stage.
 * <p>
 * It also decides which of the registered results to return for a given array of call parameters. This feature is used only in the
 * {@link MockStage#REPLAYING} stage.
 * 
 * @author marcin.jagus
 */
class ReplayState {

    /**
     * Expected result which has priority over all the other results in this state. It has the most specific matchers and will be checked first
     * against replayed call parameters.
     */
    private Chain<ExpectedCallResult> first;

    /**
     * Expected result with lowest priority. It has the most ambigous matchers and will be checked last against replayed call parameters.
     */
    private Chain<ExpectedCallResult> last;

    /**
     * Creates new state. The new state will have zero expected results registered.
     */
    public ReplayState() {
        this.first = null;
        this.last = null;
    }

    /**
     * Returns result which is expected to be returned for a given array of call parameters. An expected result will be chosen from a number of
     * {@link ExpectedCallResult} objects registered in this state. If not a single of registered objects matches the given array of parameters, an
     * {@link InterceptionResult#NOT_OVERRIDEN} object is returned.
     * 
     * @param params Given array of call parameters
     * @return Expected result for a given array of parameters or {@link InterceptionResult#NOT_OVERRIDEN} object.
     * @throws Throwable when expected result is a throwable
     */
    public InterceptionResult replayCall(Object[] params) throws Throwable {
        Chain<ExpectedCallResult> chain = first;
        Chain<ExpectedCallResult> lastMatchingChain = null;

        while (chain != null) {
            ExpectedCallResult expectedCallResult = chain.getValue();

            if (!chain.getValue().matches(params)) {
                chain = chain.getNext();
            } else if (expectedCallResult.getTimesInvoked() >= expectedCallResult.getTimesExpected()
                    && expectedCallResult.getTimesExpected() != MockBuilder.ANY_TIMES) {
                lastMatchingChain = chain;
                chain = chain.getNext();
            } else {
                break;
            }
        }

        if (chain == null) {
            chain = lastMatchingChain;
        }

        if (chain != null) {
            ExpectedCallResult value = chain.getValue();
            return new InterceptionResult(value.getResult(params));
        } else {
            return InterceptionResult.NOT_OVERRIDEN;
        }
    }

    /**
     * Registers a given expected result with this state. New result will be placed in the chain according to priority of its matchers. The more
     * specific matchers, the faster an expected result will be matched against parameters of a replayed call.
     * 
     * @param expectedResult Given expected result. Cannot be <code>null</code>.
     */
    public void addExpectedResult(ExpectedCallResult expectedResult) {
        if (expectedResult == null) {
            throw new IllegalArgumentException("expectedResult == null");
        }

        addExpectedResultByOrder(expectedResult);
    }

    /**
     * Registers a given expected result with this state. New result will be placed in the chain according to priority of its matchers. The more
     * specific matchers, the faster an expected result will be matched against parameters of a replayed call.
     * 
     * @param expectedResult Given expected result. Cannot be <code>null</code>.
     */
    private void addExpectedResultByOrder(ExpectedCallResult expectedResult) {
        if (first == null) {
            Chain<ExpectedCallResult> chain = new Chain<ExpectedCallResult>(expectedResult);

            first = chain;
            last = chain;
        } else {
            Chain<ExpectedCallResult> pivotChain = first;
            while (pivotChain.getNext() != null && pivotChain.getValue().compareTo(expectedResult) <= 0) {
                pivotChain = pivotChain.getNext();
            }

            Chain<ExpectedCallResult> newChain;
            if (expectedResult.compareTo(pivotChain.getValue()) < 0) {
                newChain = pivotChain.addBefore(expectedResult);
            } else {
                newChain = pivotChain.addAfter(expectedResult);
            }

            if (expectedResult.compareTo(first.getValue()) < 0) {
                first = newChain;
            }

            if (expectedResult.compareTo(last.getValue()) >= 0) {
                last = newChain;
            }
        }
    }

    /**
     * Verifies if all expected results where returned the expected amount of times. If at least one of the expected results wasn't returned expected
     * amount of times, an {@link MockVerifyException} exception is thrown.
     */
    public void verifyCalls() {
        Chain<ExpectedCallResult> chain = first;
        while (chain != null) {
            ExpectedCallResult expectedResult = chain.getValue();
            int timesExpected = expectedResult.getTimesExpected();
            int timesCalled = expectedResult.getTimesInvoked();

            if (timesExpected != MockBuilder.ANY_TIMES && timesExpected != timesCalled) {
                throw new MockVerifyException("Verification failed on call " + expectedResult.getRecordedCall() + ". Expected " + timesExpected
                        + " invocations but got " + timesCalled + ".");
            }

            chain = chain.getNext();
        }
    }
}
